package com.allenkerr.util;

import java.awt.image.BufferedImage;

/**
 * 感知哈希算法（Perceptual hash algorithm）
 * Created by 小柯 on 17/7/7.
 */
public class PerceptualHash {

    //pHash
    private static final int PHASH_WIDTH = 8;
    private static final int PHASH_HEIGHT = 8;

    //dHash
    private static final int DHASH_WIDTH = 9;
    private static final int DHASH_HEIGHT = 8;

    /**
     * 计算 dHash
     *
     * @param imagePath
     * @return String dHash
     */
    public static String dHash(String imagePath) {
        BufferedImage image = ImageUtils.getBufferedImage(imagePath);
        return dHash(image);
    }

    /**
     * 计算 dHash
     *
     * @param image
     * @return String dHash
     */
    public static String dHash(BufferedImage image) {
        if (image == null) return null;

        //Step1. 缩放图片：建议缩放为 9 * 8
        final int width = DHASH_WIDTH, height = DHASH_HEIGHT;
        BufferedImage thumb = ImageUtils.thumb(image, width, height, false);

        //Step2. 灰度化
        int[][] pixels = new int[height][width];
        for (int i = 0; i < height; i++) {
            for (int j = 0; j < width; j++) {
                int rgb = thumb.getRGB(j, i);
                pixels[i][j] = ImageUtils.rgbToGrey(rgb);
            }
        }

        //Step3. 差异计算
        //差异值是通过计算每行相邻像素的强度对比得出的。我们的图片为9*8的分辨率，那么就有8行，每行9个像素。
        //差异值是每行分别计算的，也就是第二行的第一个像素不会与第一行的任何像素比较。每一行有9个像素，那么
        //就会产生8个差异值，这也是为何我们选择9作为宽度，因为8bit刚好可以组成一个byte，方便转换为16进制值。
        //如果前一个像素的颜色强度大于第二个像素，那么差异值就设置为True（也就是1），如果不大于第二个像素，就设置为False（也就是0）。
        byte[][] diff = new byte[height][width - 1];
        for (int i = 0; i < height; i++) {
            for (int j = 0; j < width - 1; j++) {
                if (pixels[i][j] > pixels[i][j + 1])
                    diff[i][j] = 1;
                else
                    diff[i][j] = 0;
            }
        }

        //Step4. 转换为hash值
        //将差异值数组中每一个值看做一个bit，每8个bit组成为一个16进制值，
        //将16进制值连接起来转换为字符串，就得出了最后的dHash值。
        StringBuilder dHash = new StringBuilder();
        for (int i = 0; i < diff.length; i++) {
            String hex = MathUtils.bytesToHexString(diff[i]);
            dHash.append(hex);
        }

        return dHash.toString();
    }

    /**
     * 计算 pHash
     *
     * @param imagePath
     * @return String pHash
     */
    public static String pHash(String imagePath) {
        BufferedImage image = ImageUtils.getBufferedImage(imagePath);
        return pHash(image);
    }

    /**
     * 计算 pHash
     *
     * @param image
     * @return String pHash
     */
    public static String pHash(BufferedImage image) {
        if (image == null) return null;

        // 第一步，缩小尺寸。
        // 将图片缩小到8x8的尺寸，总共64个像素。这一步的作用是去除图片的细节，只保留结构、明暗等基本信息，
        // 摒弃不同尺寸、比例带来的图片差异。
        final int width = PHASH_WIDTH, height = PHASH_HEIGHT;
        BufferedImage thumb = ImageUtils.thumb(image, width, height, false);

        // 第二步，简化色彩。
        // 将缩小后的图片，转为64级灰度。也就是说，所有像素点总共只有64种颜色。
        int[][] pixels = new int[height][width];
        for (int i = 0; i < height; i++) {
            for (int j = 0; j < width; j++) {
                pixels[i][j] = ImageUtils.rgbToGrey(thumb.getRGB(j, i));
            }
        }

        // 第三步，计算平均值。
        // 计算所有64个像素的灰度平均值。
        int avgPixel = ImageUtils.average(pixels);

        // 第四步，比较像素的灰度。
        // 将每个像素的灰度，与平均值进行比较。大于或等于平均值，记为1；小于平均值，记为0。
        byte[][] comps = new byte[height][width];
        for (int i = 0; i < comps.length; i++) {
            for (int j = 0; j < comps[i].length; j++) {
                comps[i][j] = (byte) (pixels[i][j] >= avgPixel ? 1 : 0);
            }
        }

        // 第五步，计算哈希值。
        // 将上一步的比较结果，组合在一起，就构成了一个 8 x 8 = 64位的整数，这就是这张图片的指纹。
        // 组合的次序并不重要，只要保证所有图片都采用同样次序就行了。
        StringBuilder pHash = new StringBuilder();
        for (int i = 0; i < comps.length; i++) {
            String hex = MathUtils.bytesToHexString(comps[i]);
            pHash.append(hex);
        }

        // 得到指纹以后，就可以对比不同的图片，看看64位中有多少位是不一样的
        return pHash.toString();
    }

    /**
     * 计算汉明距离
     *
     * @param sourceHashCode
     * @param hashCode
     * @return int
     */
    public static int hammingDistance(String sourceHashCode, String hashCode) {
        if (sourceHashCode == null || hashCode == null)
            return 16;

        int distance = 0;
        int len = sourceHashCode.length();
        for (int i = 0; i < len; i++) {
            if (sourceHashCode.charAt(i) != hashCode.charAt(i))
                distance++;
        }
        return distance;
    }

    /**
     * 计算 dHash，并计算汉明距离
     *
     * @param aImage
     * @param bImage
     * @return int
     */
    public static int hammingDistanceFromDHash(BufferedImage aImage, BufferedImage bImage) {
        String aDHash = dHash(aImage);
        String bDHash = dHash(bImage);
        int distance = hammingDistance(aDHash, bDHash);
        return distance;
    }

    /**
     * 计算 dHash，并计算汉明距离
     *
     * @param aImagePath
     * @param bImagePath
     * @return int
     */
    public static int hammingDistanceFromDHash(String aImagePath, String bImagePath) {
        BufferedImage aImage = ImageUtils.getBufferedImage(aImagePath);
        BufferedImage bImage = ImageUtils.getBufferedImage(bImagePath);
        return hammingDistanceFromDHash(aImage, bImage);
    }

    /**
     * 计算 pHash，并计算汉明距离
     *
     * @param aImage
     * @param bImage
     * @return int
     */
    public static int hammingDistanceFromPHash(BufferedImage aImage, BufferedImage bImage) {
        String aDHash = pHash(aImage);
        String bDHash = pHash(bImage);
        int distance = hammingDistance(aDHash, bDHash);
        return distance;
    }

    /**
     * 计算 pHash，并计算汉明距离
     *
     * @param aImagePath
     * @param bImagePath
     * @return int
     */
    public static int hammingDistanceFromPHash(String aImagePath, String bImagePath) {
        BufferedImage aImage = ImageUtils.getBufferedImage(aImagePath);
        BufferedImage bImage = ImageUtils.getBufferedImage(bImagePath);
        return hammingDistanceFromPHash(aImage, bImage);
    }
}
